'use strict'

var escapeHTML = require('../Utils').escapeHTML

var cmdHelper = require('../helper/CmdHelper')

var domQuery = require('min-dom').query
var domAttr = require('min-dom').attr
var domClosest = require('min-dom').closest

var filter = require('lodash/filter')
var forEach = require('lodash/forEach')
var keys = require('lodash/keys')

var domify = require('min-dom').domify

var entryFieldDescription = require('./EntryFieldDescription')

var updateSelection = require('selection-update')

var TABLE_ROW_DIV_SNIPPET = '<div class="bpp-field-wrapper bpp-table-row">'
var DELETE_ROW_BUTTON_SNIPPET = '<button class="clear" data-action="deleteElement">' +
                                  '<span>X</span>' +
                                '</button>'

function createInputRowTemplate(properties, canRemove) {
  var template = TABLE_ROW_DIV_SNIPPET
  template += createInputTemplate(properties, canRemove)
  template += canRemove ? DELETE_ROW_BUTTON_SNIPPET : ''
  template += '</div>'

  return template
}

function createInputTemplate(properties, canRemove) {
  var columns = properties.length
  var template = ''
  forEach(properties, function(prop) {
    template += '<input class="bpp-table-row-columns-' + columns + ' ' +
                               (canRemove ? 'bpp-table-row-removable' : '') + '" ' +
                       'id="activiti-table-row-cell-input-value" ' +
                       'type="text" ' +
                       'name="' + escapeHTML(prop) + '" />'
  })
  return template
}

function createLabelRowTemplate(labels) {
  var template = TABLE_ROW_DIV_SNIPPET
  template += createLabelTemplate(labels)
  template += '</div>'

  return template
}

function createLabelTemplate(labels) {
  var columns = labels.length
  var template = ''
  forEach(labels, function(label) {
    template += '<label class="bpp-table-row-columns-' + columns + '">' + escapeHTML(label) + '</label>'
  })
  return template
}

function pick(elements, properties) {
  return (elements || []).map(function(elem) {
    var newElement = {}
    forEach(properties, function(prop) {
      newElement[prop] = elem[prop] || ''
    })
    return newElement
  })
}

function diff(element, node, values, oldValues, editable) {
  return filter(values, function(value, idx) {
    return !valueEqual(element, node, value, oldValues[idx], editable, idx)
  })
}

function valueEqual(element, node, value, oldValue, editable, idx) {
  if (value && !oldValue) {
    return false
  }
  var allKeys = keys(value).concat(keys(oldValue))

  return allKeys.every(function(key) {
    var n = value[key] || undefined
    var o = oldValue[key] || undefined
    return !editable(element, node, key, idx) || n === o
  })
}

function getEntryNode(node) {
  return domClosest(node, '[data-entry]', true)
}

function getContainer(node) {
  return domQuery('div[data-list-entry-container]', node)
}

function getSelection(node) {
  return {
    start: node.selectionStart,
    end: node.selectionEnd
  }
}

function setSelection(node, selection) {
  node.selectionStart = selection.start
  node.selectionEnd = selection.end
}

/**
 * @param  {Object} options
 * @param  {string} options.id
 * @param  {string} options.description
 * @param  {Array<string>} options.modelProperties
 * @param  {Array<string>} options.labels
 * @param  {Function} options.getElements - this callback function must return a list of business object items
 * @param  {Function} options.removeElement
 * @param  {Function} options.addElement
 * @param  {Function} options.updateElement
 * @param  {Function} options.editable
 * @param  {Function} options.setControlValue
 * @param  {Function} options.show
 *
 * @return {Object}
 */
module.exports = function(options) {
  var id = options.id
  var modelProperties = options.modelProperties
  var labels = options.labels
  var description = options.description

  var labelRow = createLabelRowTemplate(labels)

  var getElements = options.getElements

  var removeElement = options.removeElement
  var canRemove = typeof removeElement === 'function'

  var addElement = options.addElement
  var canAdd = typeof addElement === 'function'
  var addLabel = options.addLabel || 'Add Value'

  var updateElement = options.updateElement
  var canUpdate = typeof updateElement === 'function'

  var editable = options.editable || function() { return true }
  var setControlValue = options.setControlValue

  var show = options.show
  var canBeShown = typeof show === 'function'

  var elements = function(element, node) {
    return pick(getElements(element, node), modelProperties)
  }

  var factory = {
    id: id,
    html: (canAdd
      ? '<div class="bpp-table-add-row" ' + (canBeShown ? 'data-show="show"' : '') + '>' +
            '<label>' + escapeHTML(addLabel) + '</label>' +
            '<button class="add" data-action="addElement"><span>+</span></button>' +
          '</div>' : '') +
          '<div class="bpp-table" data-show="showTable">' +
            '<div class="bpp-field-wrapper bpp-table-row">' +
               labelRow +
            '</div>' +
            '<div data-list-entry-container>' +
            '</div>' +
          '</div>' +

          // add description below table entry field
          (description ? entryFieldDescription(description) : ''),

    get: function(element, node) {
      var boElements = elements(element, node, this.__invalidValues)

      var invalidValues = this.__invalidValues

      delete this.__invalidValues

      forEach(invalidValues, function(value, idx) {
        var element = boElements[idx]

        forEach(modelProperties, function(prop) {
          element[prop] = value[prop]
        })
      })

      return boElements
    },

    set: function(element, values, node) {
      var action = this.__action || {}
      delete this.__action

      if (action.id === 'delete-element') {
        return removeElement(element, node, action.idx)
      } else if (action.id === 'add-element') {
        return addElement(element, node)
      } else if (canUpdate) {
        var commands = []
        var valuesToValidate = values

        if (typeof options.validate !== 'function') {
          valuesToValidate = diff(element, node, values, elements(element, node), editable)
        }

        var self = this

        forEach(valuesToValidate, function(value) {
          var validationError
          var idx = values.indexOf(value)

          if (typeof options.validate === 'function') {
            validationError = options.validate(element, value, node, idx)
          }

          if (!validationError) {
            var cmd = updateElement(element, value, node, idx)

            if (cmd) {
              commands.push(cmd)
            }
          } else {
            // cache invalid value in an object by index as key
            self.__invalidValues = self.__invalidValues || {}
            self.__invalidValues[idx] = value

            // execute a command, which does not do anything
            commands.push(cmdHelper.updateProperties(element, {}))
          }
        })

        return commands
      }
    },
    createListEntryTemplate: function(value, index, selectBox) {
      return createInputRowTemplate(modelProperties, canRemove)
    },

    addElement: function(element, node, event, scopeNode) {
      var template = domify(createInputRowTemplate(modelProperties, canRemove))

      var container = getContainer(node)
      container.appendChild(template)

      this.__action = {
        id: 'add-element'
      }

      return true
    },

    deleteElement: function(element, node, event, scopeNode) {
      var container = getContainer(node)
      var rowToDelete = event.delegateTarget.parentNode
      var idx = parseInt(domAttr(rowToDelete, 'data-index'), 10)

      container.removeChild(rowToDelete)

      this.__action = {
        id: 'delete-element',
        idx: idx
      }

      return true
    },

    editable: function(element, rowNode, input, prop, value, idx) {
      var entryNode = domClosest(rowNode, '[data-entry]')
      return editable(element, entryNode, prop, idx)
    },

    show: function(element, entryNode, node, scopeNode) {
      entryNode = getEntryNode(entryNode)
      return show(element, entryNode, node, scopeNode)
    },

    showTable: function(element, entryNode, node, scopeNode) {
      entryNode = getEntryNode(entryNode)
      var elems = elements(element, entryNode)
      return elems && elems.length && (!canBeShown || show(element, entryNode, node, scopeNode))
    },

    validateListItem: function(element, value, node, idx) {
      if (typeof options.validate === 'function') {
        return options.validate(element, value, node, idx)
      }
    }

  }

  // Update/set the selection on the correct position.
  // It's the same code like for an input value in the PropertiesPanel.js.
  if (setControlValue) {
    factory.setControlValue = function(element, rowNode, input, prop, value, idx) {
      var entryNode = getEntryNode(rowNode)

      var isReadOnly = domAttr(input, 'readonly')
      var oldValue = input.value

      var selection

      // prevents input fields from having the value 'undefined'
      if (value === undefined) {
        value = ''
      }

      // when the attribute 'readonly' exists, ignore the comparison
      // with 'oldValue' and 'value'
      if (!!isReadOnly && oldValue === value) {
        return
      }

      // update selection on undo/redo
      if (document.activeElement === input) {
        selection = updateSelection(getSelection(input), oldValue, value)
      }

      setControlValue(element, entryNode, input, prop, value, idx)

      if (selection) {
        setSelection(input, selection)
      }
    }
  }

  return factory
}
